﻿using System;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace Xtensible.Extensions.Mapping
{
    public static class Mapper
    {
        /// <summary>
        ///   Maps source properties to destination properties with a different name
        /// </summary>
        /// <param name = "o">Source object</param>
        /// <param name = "sourceName">Source property name</param>
        /// <param name = "destinationName">Destination property name</param>
        /// <returns></returns>
        public static MapAction WithAlias(this object o, string destinationName, string sourceName)
        {
            var mapAction = GetMapAction(o);
            if (mapAction.Aliases == null)
                mapAction.Aliases = new Aliases();

            mapAction.Aliases.Add(destinationName, sourceName);

            return mapAction;
        }


        /// <summary>
        ///   Maps source properties to destination properties with a different name
        /// </summary>
        /// <typeparam name = "S"></typeparam>
        /// <typeparam name = "D"></typeparam>
        /// <param name = "o"></param>
        /// <param name = "sourceExpression"></param>
        /// <param name = "destinationExpression"></param>
        /// <returns></returns>
        public static MapAction WithAlias<D, S>(this object o, Expression<Func<D, object>> destinationExpression,
                                                Expression<Func<S, object>> sourceExpression)
        {
            var mapAction = GetMapAction(o);

            try
            {
                var destination = Helper.GetPropertyNameFromExpression(destinationExpression);
                var source = Helper.GetPropertyNameFromExpression(sourceExpression);

                if (mapAction.Aliases == null)
                    mapAction.Aliases = new Aliases();

                mapAction.Aliases.Add(destination, source);
            }
            catch (Exception ex)
            {
                throw new ArgumentException("Only properties can be aliased", ex);
            }

            return mapAction;
        }

        /// <summary>
        ///   Combines WithAlias and WithPropertyNameTrigger; it maps source properties with destination properties with a different name and then applies trigger rules
        /// </summary>
        /// <param name = "?"></param>
        /// <param name = "destinationName"></param>
        /// <param name = "sourceName"></param>
        /// <returns></returns>
        public static MapAction WithAliasTrigger(this object o, string destinationName, string sourceName,
                                                 Func<object, object> triggerAction)
        {
            return WithAlias(o, destinationName, sourceName).WithPropertyNameTrigger(sourceName, triggerAction);
        }

        /// <summary>
        ///   Combines WithAlias and WithPropertyNameTrigger; it maps source properties with destination properties with a different name and then applies trigger rules
        /// </summary>
        /// <typeparam name = "D"></typeparam>
        /// <typeparam name = "S"></typeparam>
        /// <param name = "o"></param>
        /// <param name = "destinationExpression"></param>
        /// <param name = "sourceExpression"></param>
        /// <param name = "triggerAction"></param>
        /// <returns></returns>
        public static MapAction WithAliasTrigger<D, S>(this object o, Expression<Func<D, object>> destinationExpression,
                                                       Expression<Func<S, object>> sourceExpression,
                                                       Func<object, object> triggerAction)
        {
            return WithAlias(o, destinationExpression, sourceExpression).WithPropertyNameTrigger(sourceExpression,
                                                                                                 triggerAction);
        }

        /// <summary>
        ///   Transforms data before copying to destination for specified type
        /// </summary>
        /// <param name = "o"></param>
        /// <param name = "type"></param>
        /// <param name = "triggerAction"></param>
        /// <returns></returns>
        public static MapAction WithTypeTrigger(this object o, Type type, Func<object, object> triggerAction)
        {
            var mapAction = GetMapAction(o);
            if (mapAction.DataTypeTriggers == null)
                mapAction.DataTypeTriggers = new DataTypeTriggers();

            mapAction.DataTypeTriggers.Add(type, triggerAction);

            return mapAction;
        }

        /// <summary>
        ///   Transforms data before copying to destination for specified property
        /// </summary>
        /// <param name = "o"></param>
        /// <param name = "propertyName"></param>
        /// <param name = "triggerAction"></param>
        /// <returns></returns>
        public static MapAction WithPropertyNameTrigger(this object o, string propertyName,
                                                        Func<object, object> triggerAction)
        {
            var mapAction = GetMapAction(o);
            if (mapAction.PropertyNameTriggers == null)
                mapAction.PropertyNameTriggers = new PropertyNameTriggers();

            mapAction.PropertyNameTriggers.Add(propertyName, triggerAction);

            return mapAction;
        }

        /// <summary>
        ///   Transforms data before copying to destination for specified property
        /// </summary>
        /// <typeparam name = "S"></typeparam>
        /// <param name = "o"></param>
        /// <param name = "sourceExpression"></param>
        /// <param name = "triggerAction"></param>
        /// <returns></returns>
        public static MapAction WithPropertyNameTrigger<S>(this object o, Expression<Func<S, object>> sourceExpression,
                                                           Func<object, object> triggerAction)
        {
            var mapAction = GetMapAction(o);
            try
            {
                var source = Helper.GetPropertyNameFromExpression(sourceExpression);

                if (mapAction.PropertyNameTriggers == null)
                    mapAction.PropertyNameTriggers = new PropertyNameTriggers();

                mapAction.PropertyNameTriggers.Add(source, triggerAction);
            }
            catch (Exception ex)
            {
                throw new ArgumentException("Cannot determine property from expression", ex);
            }

            return mapAction;
        }

        /// <summary>
        ///   Excludes source property from being mapped
        /// </summary>
        /// <param name = "o"></param>
        /// <param name = "propertyName"></param>
        /// <returns></returns>
        public static MapAction WithExclusion(this object o, string propertyName)
        {
            var mapAction = GetMapAction(o);
            if (mapAction.Exclusions == null)
                mapAction.Exclusions = new Exclusions();

            mapAction.Exclusions.Add(propertyName);

            return mapAction;
        }

        /// <summary>
        ///   Excludes source property from being mapped
        /// </summary>
        /// <typeparam name = "S"></typeparam>
        /// <param name = "o"></param>
        /// <param name = "sourceExpression"></param>
        /// <returns></returns>
        public static MapAction WithExclusion<S>(this object o, Expression<Func<S, object>> sourceExpression)
        {
            var mapAction = GetMapAction(o);
            try
            {
                var source = Helper.GetPropertyNameFromExpression(sourceExpression);

                if (mapAction.Exclusions == null)
                    mapAction.Exclusions = new Exclusions();

                mapAction.Exclusions.Add(source);
            }
            catch (Exception ex)
            {
                throw new ArgumentException("Only properties can be excluded", ex);
            }

            return mapAction;
        }

        //used when called directly by object (no chaining)
        public static T Map<T>(this object source) where T : new()
        {
            var t = new T();
            return Map(source, t);
        }

        public static T Map<T>(this object source, T destination)
        {
            var ma = GetMapAction(source);

            return ma.Map(destination);
        }

        /// <summary>
        ///   Maps, using configuration for each item in the collection
        /// </summary>
        /// <typeparam name = "T"></typeparam>
        /// <param name = "sourceEnumeration"></param>
        /// <returns></returns>
        public static EnumerationMapAction ForGroup<T>(this IEnumerable<T> sourceEnumeration) where T : class, new()
        {
            var ema = new EnumerationMapAction
                          {Source = sourceEnumeration, SourceType = typeof (T), SourceEnumaration = sourceEnumeration};
            return ema;
        }

        private static MapAction GetMapAction(object o)
        {
            var mapAction = o as MapAction;
            if (mapAction != null)
                return mapAction;
            else
            {
                var ma = new MapAction { SourceType = o.GetType(), Source = o };
                return ma;
            }
        }

        /// <summary>
        ///   Flattens 2nd level objects based on naming convention.
        /// </summary>
        /// <example>
        ///   Assuming you have a destination class with SupervisorFirstName, 
        ///   source object employee with a child object of Supervisor, which contains property FirstName - SupervisorFirstName will be mapped with the value
        ///   of employee.Supervisor.FirstName
        /// </example>
        /// <param name = "sourceProperty"></param>
        /// <returns></returns>
        public static MapAction Flatten(this object source, string sourceProperty)
        {
            return Flatten(source, sourceProperty, sourceProperty);
        }

        /// <summary>
        ///   Flattens 2nd level objects based on naming convention.
        /// </summary>
        /// <example>
        ///   Assuming you have a destination class with SupervisorFirstName, 
        ///   source object employee with a child object of Supervisor, which contains property FirstName - SupervisorFirstName will be mapped with the value
        ///   of employee.Supervisor.FirstName
        /// </example>
        /// <param name = "sourceProperty"></param>
        /// <returns></returns>
        public static MapAction Flatten<S>(this object source, Expression<Func<S, object>> sourceExpression)
        {
            try
            {
                var propertyName = Helper.GetPropertyNameFromExpression(sourceExpression);

                return Flatten(source, propertyName);
            }
            catch (Exception ex)
            {
                throw new ArgumentException("Cannot determine property from expression", ex);
            }
        }


        /// <summary>
        ///   Flattens 2nd level objects based on naming convention.
        /// </summary>
        /// <example>
        ///   Assuming you have a destination class with SupFirstName, 
        ///   source object employee with a child object of Supervisor, which contains property FirstName - SupFirstName will be mapped with the value
        ///   of employee.Supervisor.FirstName if the prefix of "Sup" is provided
        /// </example>
        /// <param name = "source"></param>
        /// <param name = "sourceProperty"></param>
        /// <param name = "destinationPropertyPrefix"></param>
        /// <returns></returns>
        public static MapAction Flatten(this object source, string sourceProperty, string destinationPropertyPrefix)
        {
            return Flatten(source, sourceProperty, destinationPropertyPrefix, null);
        }


        /// <summary>
        ///   Flattens 2nd level objects based on naming convention.
        /// </summary>
        /// <example>
        ///   Assuming you have a destination class with SupervisorFirstName, 
        ///   source object employee with a child object of Supervisor, which contains property FirstName - SupervisorFirstName will be mapped with the value
        ///   of employee.Supervisor.FirstName
        /// </example>
        /// <param name = "sourceProperty"></param>
        /// <returns></returns>
        public static MapAction Flatten<S>(this object source, Expression<Func<S, object>> sourceExpression,
                                           string destinationPropertyPrefix)
        {
            try
            {
                var propertyName = Helper.GetPropertyNameFromExpression(sourceExpression);

                return Flatten(source, propertyName, destinationPropertyPrefix);
            }
            catch (Exception ex)
            {
                throw new ArgumentException("Cannot determine property from expression", ex);
            }
        }

        /// <summary>
        ///   Flattens 2nd level objects based on naming convention.
        /// </summary>
        /// <example>
        ///   Assuming you have a destination class with SupFirstName, 
        ///   source object employee with a child object of Supervisor, which contains property FirstName - SupFirstName will be mapped with the value
        ///   of employee.Supervisor.FirstName if the prefix of "Sup" is provided
        /// </example>
        /// <param name = "sourceProperty"></param>
        /// <param name = "destinationPropertyPrefix"></param>
        /// <param name = "excludedDestinationProperties">Excluded destination properties will not be mapped</param>
        /// <returns></returns>
        public static MapAction Flatten(this object source, string sourceProperty, string destinationPropertyPrefix,
                                        List<string> excludedDestinationProperties)
        {
            var ma = GetMapAction(source);
            if (ma.FlattenPrefixes == null)
                ma.FlattenPrefixes = new Dictionary<string, string>();

            if (excludedDestinationProperties != null)
            {
                if (ma.FlattenExclusions == null)
                    ma.FlattenExclusions = new Dictionary<string, List<string>>();

                ma.FlattenExclusions.Add(sourceProperty, excludedDestinationProperties);
            }
            ma.FlattenPrefixes.Add(sourceProperty, destinationPropertyPrefix);
            return ma;
        }

        /// <summary>
        ///   Flattens 2nd level objects based on naming convention.
        /// </summary>
        /// <example>
        ///   Assuming you have a destination class with SupervisorFirstName, 
        ///   source object employee with a child object of Supervisor, which contains property FirstName - SupervisorFirstName will be mapped with the value
        ///   of employee.Supervisor.FirstName
        /// </example>
        /// <param name = "sourceProperty"></param>
        /// <returns></returns>
        public static MapAction Flatten<S>(this object source, Expression<Func<S, object>> sourceExpression,
                                           string destinationPropertyPrefix, List<string> excludedDestinationProperties)
        {
            try
            {
                var propertyName = Helper.GetPropertyNameFromExpression(sourceExpression);

                return Flatten(source, propertyName, destinationPropertyPrefix, excludedDestinationProperties);
            }
            catch (Exception ex)
            {
                throw new ArgumentException("Cannot determine property from expression", ex);
            }
        }

        /// <summary>
        ///   Custom or complex flattening rules can be specified here.  This runs before any other flattening procedure.
        /// </summary>
        /// <param name = "source"></param>
        /// <param name = "flattenFunc"></param>
        /// <returns></returns>
        public static MapAction Flatten(this object source, Func<object, object> flattenFunc)
        {
            var ma = GetMapAction(source);

            if (ma.FlatteningFunc != null)
                throw new InvalidOperationException("FlattenFunc can only be set once");

            ma.FlatteningFunc = flattenFunc;

            return ma;
        }
    }
}